package epidemic;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.Random;



/**
 * A node actively spreading infection to other nodes.
 * 
 * Three states:
 * Infected. Node is infected and is actively spreading infection.
 * Susceptible. Node is not infected, but is susceptible to infection.
 * Removed. Immune to infection. The node has been infected, and has completed spreading infection.
 * 
 * @author olais
 *
 */
public class PushNode {
	
	// The node state
	public enum State { INFECTED, SUSCEPTIBLE, REMOVED };
	State state;
	
	// The node lifetime in steps and spread frequency
	int n;
	int f;
	
	// The position and the radius of the node on screen
	Vec pos;
	double r;
	
	// The nodes and other nodes recently infected by us
	ArrayList<PushNode> members;
	ArrayList<PushNode> recentInfected;
	
	PushNode(ArrayList<PushNode> members, int n, int f) {
		this.state = State.SUSCEPTIBLE;
		
		this.n = n;
		this.f = f;
		
		this.members = members;
		this.recentInfected = new ArrayList<PushNode>(f);
		
		// Default position and radius
		this.pos = new Vec(0, 0);
		this.r = 1;
	}
	
	public void setPos(Vec pos) {
		this.pos = pos;
	}
	
	public void setRadius(double r) {
		this.r = r;
	}
	
	public void paint(Graphics2D g) {
		AffineTransform tr = g.getTransform();
		
		// Paint the node in the color according to its state
		// (infected: red, susceptible: blue, removed: black)
		Color c = state == State.INFECTED ? Color.RED : state == State.SUSCEPTIBLE ? Color.BLUE : Color.BLACK;
		g.setColor(c);
		g.fillOval((int) (pos.x-r), (int) (pos.y-r), (int) (r*2), (int) (r*2));
		
		// Paint arrows to nodes recently infected
		if (EpiFrame.settings.arrows) {
			for (PushNode recent : recentInfected) {
				
				// Point to surface, not center
				Vec line = recent.pos.sub(this.pos);
				Vec tip = this.pos.add(line.unit().mul(line.length() - recent.r));
				
				// Paint it
				g.setColor(Color.MAGENTA);
				g.setStroke(new BasicStroke(3));
				EpiUtils.drawArrow(g, pos, tip, 7);
			}
		}
		
		g.setTransform(tr);
	}
	
	public void infect() {
		// Only infect nodes susceptible to infection
		if (this.state == State.SUSCEPTIBLE) {
			this.state = State.INFECTED;
		}
	}
	
	public boolean isInfected() {
		return this.state == State.INFECTED;
	}
	
	public boolean isSusceptible() {
		return this.state == State.SUSCEPTIBLE;
	}
	
	public boolean isRemoved() {
		return this.state == State.REMOVED;
	}
	
	public void clearRecent() {
		this.recentInfected.clear();
	}
	
	public void step() {
		// If infected, attempt to infect others
		if (state == State.INFECTED) {
			
			// ... for n rounds
			if (n-- > 0) {
				
				// ... potentially infecting f other nodes
				for (int i = f; i > 0; i--) {
					
					// Determine which node to infect based on the infection strategy
					PushNode node = 
							EpiFrame.settings.strategy == EpiSettings.Strategy.RANDOM ? findRandomCandidate() : 
							EpiFrame.settings.strategy == EpiSettings.Strategy.CLOSEST ? findClosestCandidate() : 
							EpiFrame.settings.strategy == EpiSettings.Strategy.CLOSEST_SUSCEPTIBLE ? findClosestSusceptibleCandidate() : 
							EpiFrame.settings.strategy == EpiSettings.Strategy.CLOSEST_UNREMOVED ? findClosestUnremovedCandidate() : 
							EpiFrame.settings.strategy == EpiSettings.Strategy.CLOSER_THAN_N ? findCloserThanNCandidate(EpiFrame.settings.threshold) : null;
					
					// If no candidate was found, continue
					if (node == null)
						continue;
					
					// Infect node and add to the recently infected list
					node.infect();
					recentInfected.add(node);
				}
			} else {
				// Become immune after n rounds
				this.state = State.REMOVED;
			}
		}
	}
	
	public PushNode findRandomCandidate() {
		while (true) {
			
			// Pick a random node
			PushNode node = members.get(new Random().nextInt(members.size()));
			
			// Do not pick ourself
			if (node == this)
				continue;
			
			// Do not pick again
			if (recentInfected.contains(node))
				continue;
			
			return node;
		}
	}
	
	public PushNode findClosestCandidate() {
		PushNode best = null;
		double min = Integer.MAX_VALUE;
		
		for (PushNode node : members) {

			// Do not pick ourself
			if (node == this)
				continue;
			
			// Do not pick again
			if (recentInfected.contains(node))
				continue;
			
			double len = node.pos.sub(this.pos).length();
			if (len < min) {
				best = node;
				min = len;
			}
		}
		
		return best;
	}
	
	public PushNode findClosestSusceptibleCandidate() {
		PushNode best = null;
		double min = Integer.MAX_VALUE;
		
		for (PushNode node : members) {

			// Do not pick ourself
			if (node == this)
				continue;
			
			// Do not pick again
			if (recentInfected.contains(node))
				continue;
			
			double len = node.pos.sub(this.pos).length();
			if (len < min && node.isSusceptible()) {
				best = node;
				min = len;
			}
		}
		
		return best;
	}
	
	public PushNode findClosestUnremovedCandidate() {
		PushNode best = null;
		double min = Integer.MAX_VALUE;
		
		for (PushNode node : members) {

			// Do not pick ourself
			if (node == this)
				continue;
			
			// Do not pick again
			if (recentInfected.contains(node))
				continue;
			
			double len = node.pos.sub(this.pos).length();
			if (len < min && !node.isRemoved()) {
				best = node;
				min = len;
			}
		}
		
		return best;
	}
	
	public PushNode findCloserThanNCandidate(double n) {
		PushNode best = null;
		double min = Integer.MAX_VALUE;
		
		for (PushNode node : members) {

			// Do not pick ourself
			if (node == this)
				continue;
			
			// Do not pick again
			if (recentInfected.contains(node))
				continue;
			
			double len = node.pos.sub(this.pos).length();
			if (len < min && len < n) {
				best = node;
				min = len;
			}
		}
		
		return best;
	}
}
